• 问题

    在枚举类型出现之前,一般都常常使用int常量或者String常量表示列举相关事物。如:

    public static final int APPLE_FUJI = 0;
    public static final int APPLE_PIPPIN = 1;
    public static final int APPLE_GRANNY_SMITH = 2;
    
    public static final int ORANGE_NAVEL = 0;
    public static final int ORANGE_TEMPLE = 1;
    public static final int ORANGE_BLOOD = 2;1234567
    

    针对int常量以下不足:

    1. 在类型安全方面,如果你想使用的是ORANGE_NAVEL,但是传递是APPLE_FUJI,编译器并不能检测出错误;
    2. 因为int常量是编译时常量,被编译到使用它们的客户端中。若与枚举常量关联的int发生了变化,客户端需重新编译,否则它们的行为就不确定;
    3. 没有便利方法将int常量翻译成可打印的字符串。这里的意思应该是比如你想调用的是ORANGE_NAVEL,debug的时候显示的是0,但你不能确定是APPLE_FUJI还是ORANGE_NAVEL
    4. 如果你想使用String常量,虽然可以打印,但是字符串的比较是对性能有较大的影响的。另外,容易将字符串硬编码到代码之中;

    那么,针对这样的情况应该怎样解决?

  • 解决

    针对这样的情况,可以采用enum来解决。

    1. enum使用方法

      • enum默认构建

        以上面的APPLEORANGE为例,用enum重写:

        public enum Apple {
            APPLE_FUJI,
            APPLE_PIPPIN,
            APPLE_GRANNY_SMITH;
        }
        
        public enum Orange {
            ORANGE_NAVEL,
            ORANGE_TEMPLE,
            ORANGE_BLOOD;
        }
        

        在调用的时候,直接使用enum类型,在编译的时候可以直接指定类型,否则编译不通过;并且debug的时候,显示的是enum中的常量(APPLE_FUJI这样的),可以一眼看出是否用错;最后由于枚举导出的常量域(APPLE_FUJI等)与客户端之间是通过枚举来引用的,再增加或者重排序枚举类型中的常量后,并不需要重新编译客户端代码

      • enum枚举数据域和方法

        enum类中同样可以有自己的数据域方法

        如以太阳系为例,每个行星都拥有质量和半径,可以依据这两个属性计算行星表面物体的重量。代码如下:

        public enum Planet {
            MERCURY(3.302e+23, 2.439e6),
            VENUS (4.869e+24, 6.052e6),
            EARTH (5.975e+24, 6.378e6),
            MARS (6.419e+23, 3.393e6),
            JUPITER(1.899e+27, 7.149e7),
            SATURN (5.685e+26, 6.027e7),
            URANUS (8.683e+25, 2.556e7),
            NEPTUNE(1.024e+26, 2.477e7);
        
            private final double mass; // In kilograms
            private final double radius; // In meters
            private final double surfaceGravity; // In m / s^2
        
            // Universal gravitational constant in m^3 / kg s^2
            private static final double G = 6.67300E-11;
        
            // Constructor
            Planet(double mass, double radius) {
                this.mass = mass;
                this.radius = radius;
                surfaceGravity = G * mass / (radius * radius);
            }
        
            public double mass() { return mass; }
            public double radius() { return radius; }
            public double surfaceGravity() { 
                return surfaceGravity; 
            }
        
            public double surfaceWeight(double mass) {
                return mass * surfaceGravity; // F = ma
            }
        }
        
        public class PlanetDemo {
            public static void main(String[] args) {
                double earthWeight = Double.parseDouble(args[0]);
                double mass = earthWeight / Planet.EARTH.surfaceGravity();
        
                for (Planet p : Planet.values()) {
                    System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
                }
        
                //args[0]=30输出结果
                //Weight on MERCURY is 11.337201
                //Weight on VENUS is 27.151530
                //Weight on EARTH is 30.000000
                //Weight on MARS is 11.388120
                //Weight on JUPITER is 75.890383
                //Weight on SATURN is 31.965423
                //Weight on URANUS is 27.145664
                //Weight on NEPTUNE is 34.087906
            }
        }
        
    2. enum枚举常量来分发不同的方法

      有时候会使用枚举中的值来作为逻辑条件来分发至不同的方法,如采用枚举来写加、减、乘、除的运算。代码如下:

      public enum Operation {
          PLUS, MINUS, TIMES, DIVIDE;
      
          double apply(double x, double y) {
              switch(this) {
                  case PLUS: return x + y;
                  case MINUS: return x - y;
                  case TIMES: return x * y;
                  case DIVIDE: return x / y;
              }
              throw new AssertionError("Unknown op: " + this);
          }
      }
      

      大家一开始都会这样写的。实际开发中,有很多开发者也这样写。但是有个不足:如果需要新增加运算,譬如模运算,不仅仅需要添加枚举类型常量,还需要修改apply方法。万一忘记修改了,那就是运行时错误。将代码修改如下:

      public enum Operation {
        PLUS {
          @Override
          double apply(double x, double y) {
            return x + y;
          }
        },
      
        MINUS {
          @Override
          double apply(double x, double y) {
            return x - y;
          }
        },
      
        TIMES {
          @Override
          double apply(double x, double y) {
            return x * y;
          }
        },
      
        DIVIDE {
          @Override
          double apply(double x, double y) {
            return x / y;
          }
        };
      
        abstract double apply(double x, double y);
      }
      

      每次新增加运算种类,都需要重写apply方法,这样就不会有遗漏修改。

      你可以写的更详细些:

      public enum Operation {
        PLUS("+") {
          @Override
          double apply(double x, double y) {
            return x + y;
          }
        },
      
        MINUS("-") {
          @Override
          double apply(double x, double y) {
            return x - y;
          }
        },
      
        TIMES("*") {
          @Override
          double apply(double x, double y) {
            return x * y;
          }
        },
      
        DIVIDE("/") {
          @Override
          double apply(double x, double y) {
            return x / y;
          }
        };
      
        private String symbol;
        Operation(String symbol) {
          this.symbol = symbol;
        }
      
        @Override
        public String toString() {
          return symbol;
        }
      
        abstract double apply(double x, double y);
      }
      
      public class OperationDemo {
      
        public static void main(String[] args) {
          double x = Double.parseDouble(args[0]);
          double y = Double.parseDouble(args[1]);
      
          for (Operation op : Operation.values()) {
            System.out.println(String.format("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
          }
      
          //输入2 4
          //2.000000 + 4.000000 = 6.000000
          //2.000000 - 4.000000 = -2.000000
          //2.000000 * 4.000000 = 8.000000
          //2.000000 / 4.000000 = 0.500000
        }
      }
      

      一般,enum中重写了toString方法之后,enum中自生成的valueOf(String)方法不能根据枚举常量的字符串(toString生成)来获取枚举常量。我们通常需要在enum中新增个静态常量来获取。如:

      public enum Operation {
        PLUS("+") {
          @Override
          double apply(double x, double y) {
            return x + y;
          }
        },
      
        MINUS("-") {
          @Override
          double apply(double x, double y) {
            return x - y;
          }
        },
      
        TIMES("*") {
          @Override
          double apply(double x, double y) {
            return x * y;
          }
        },
      
        DIVIDE("/") {
          @Override
          double apply(double x, double y) {
            return x / y;
          }
        };
      
        private String symbol;
        public static final Map<String, Operation> OPERS_MAP = Maps.newHashMap();
      
        static {
          for (Operation op : Operation.values()) {
            OPERS_MAP.put(op.toString(), op);
          }
        }
      
        Operation(String symbol) {
          this.symbol = symbol;
        }
      
        @Override
        public String toString() {
          return symbol;
        }
      
        abstract double apply(double x, double y);
      }
      

    可以通过调用Operation.OPERS_MAP.get(op.toString())来获取对应的枚举常量。

    在有些特定的情况下,此写法有个缺点,即如果每个枚举常量都有公共的部分处理该怎么办,如果每个枚举常量关联的方法里都有公共的部分,那不仅不美观,还违反了DRY原则。这就是下面的枚举策略模式。

  • 枚举策略模式

    直接上例子来分析:

    enum PayrollDay {
        MONDAY, 
        TUESDAY, 
        WEDNESDAY, 
        THURSDAY, 
        FRIDAY,
        SATURDAY, 
        SUNDAY;
    
        private static final int HOURS_PER_SHIFT = 8;
    
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
    
            double overtimePay; // Calculate overtime pay
            switch(this) {
                case SATURDAY: case SUNDAY:
                    overtimePay = hoursWorked * payRate / 2;
                    break;
                default: // Weekdays
                    overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            }
    
            return basePay + overtimePay;
        }
    }
    

    以上代码是计算工人工资。平时工作8小时,超过8小时,以加班工资方式另外计算;如果是双休日,都按照加班方式处理工资。上面代码的写法和上一小节给出的差不多,通过switch来分拆计算。还是一样的问题,如果此时新增加一种工资的计算方式,枚举常量需要改,pay方法也需要改。按上一小节的介绍继续修改:

    enum PayrollDay {
        MONDAY {
            @Override
            double pay(double hoursWorked, double payRate) {
                double basePay = hoursWorked * payRate;
                double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                        0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                return basePay + overtimePay;
            }
        }, 
        TUESDAY {
            @Override
            double pay(double hoursWorked, double payRate) {
                double basePay = hoursWorked * payRate;
                double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                        0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                return basePay + overtimePay;
            }
        }, 
        WEDNESDAY {
            @Override
            double pay(double hoursWorked, double payRate) {
                double basePay = hoursWorked * payRate;
                double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                        0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                return basePay + overtimePay;
            }
        }, 
        THURSDAY {
            @Override
            double pay(double hoursWorked, double payRate) {
                double basePay = hoursWorked * payRate;
                double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                        0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                return basePay + overtimePay;
            }
        }, 
        FRIDAY {
            @Override
            double pay(double hoursWorked, double payRate) {
                double basePay = hoursWorked * payRate;
                double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                        0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
                return basePay + overtimePay;
            }
        },
        SATURDAY {
            @Override
            double pay(double hoursWorked, double payRate) {
                double basePay = hoursWorked * payRate;
                double overtimePay = overtimePay = hoursWorked * payRate / 2;
                return basePay + overtimePay;
            }
        }, 
        SUNDAY {
            @Override
            double pay(double hoursWorked, double payRate) {
                double basePay = hoursWorked * payRate;
                double overtimePay = overtimePay = hoursWorked * payRate / 2;
                return basePay + overtimePay;
            }
        }, ;
    
        private static final int HOURS_PER_SHIFT = 8;
    
        abstract double pay(double hoursWorked, double payRate);
    }
    

    看了上面的代码,我觉得大家都不会这样写吧。其实细想一下,最主要的不同就是计算加班时间的工资方式不同,也就是分工作日和双休日的。继续修改:

    public enum PayRoll {
      MONDY(PayType.WEEKDAY),
      TUESDAY(PayType.WEEKDAY),
      WEDNESDAY(PayType.WEEKDAY),
      THURSDAY(PayType.WEEKDAY),
      FRIDAY(PayType.WEEKDAY),
      SATURDAY(PayType.WEEKEND),
      SUNDAY(PayType.WEEKEND);
    
      private final PayType payType;
      PayRoll(PayType payType) {
        this.payType = payType;
      }
    
      double pay(double hoursWorked, double payRate) {
        return payType.pay(hoursWorked, payRate);
      }
    
      private enum PayType {
        WEEKDAY {
          @Override
          double overtimePay(double hoursWorked, double payRate) {
            double overtime = hoursWorked - HOURS_PER_SHIFT;
            return overtime <= 0 ? 0 : overtime * payRate / 2;
          }
        },
    
        WEEKEND {
          @Override
          double overtimePay(double hoursWorked, double payRate) {
            return hoursWorked * payRate / 2;
          }
        };
    
        private static final int HOURS_PER_SHIFT = 8;
        abstract double overtimePay(double hoursWorked, double payRate);
    
        double pay(double hoursWorked, double payRate) {
          double basePay = hoursWorked * payRate;
          return basePay + overtimePay(hoursWorked, payRate);
        }
      }
    }
    

    虽然看起来代码不够简洁,但是修改起来确实比较安全,不怕有遗漏。

    补充一点

    从上面可以看出,不推荐在enum中使用switch...case...来判断不同的行为。那什么时候可以使用呢?主要是适用于给外部的枚举类型增加特定于常量的行为。如,假设Operation枚举不受开发者自己控制,但是希望它有一个实例方法来返回每个运算的反运算,则可以:

    public static Operation inverse(Operation op) {
        switch(op) {
            case PLUS: return Operation.MINUS;
            case MINUS: return Operation.PLUS;
            case TIMES: return Operation.DIVIDE;
            case DIVIDE: return Operation.TIMES;
            default: throw new AssertionError("Unknown op: " + op);
        }
    }
    
  • 结论

    与int常量相比,枚举的可读性更强,并且更加安全,功能更加强大,在实际开发中应该使用enum代替int枚举模式。

results matching ""

    No results matching ""